package be.nepherte.movmanager.domain;

import java.io.Serializable;
import java.util.Date;
import java.util.List;

import javax.jdo.annotations.Element;
import javax.jdo.annotations.Extension;
import javax.jdo.annotations.IdGeneratorStrategy;
import javax.jdo.annotations.Order;
import javax.jdo.annotations.PersistenceCapable;
import javax.jdo.annotations.Persistent;
import javax.jdo.annotations.PrimaryKey;
import javax.jdo.annotations.IdentityType;

import be.nepherte.movmanager.client.exceptions.InvalidMovieDurationException;
import be.nepherte.movmanager.client.exceptions.InvalidMovieRatingException;
import be.nepherte.movmanager.client.exceptions.InvalidMovieReleaseDateException;
import be.nepherte.movmanager.client.exceptions.InvalidMovieTitleException;

import com.google.appengine.api.datastore.Key;

/**
 * This class represents a movie. Each movie has a few required fields:
 * <ul>
 * 		<li>unique key id</li>
 * 		<li>title</li>
 * 		<li>movie collection</li>
 * </ul>
 * 
 * and many optional fields:
 * <ul>
 * 		<li>duration</li>
 * 		<li>rating</li>
 * 		<li>description</li>
 * 		<li>release date</li>
 * 		<li>directors</li>
 * 		<li>writers</li>
 * 		<li>actors</li>
 * 		<li>studio</li>
 * 		<li>genres</li>
 * 		<li>watched indicator</li>
 * 		<li>movie poster</li>
 * </ul>
 * Each movie can be stored in a Java Data Objects (JDO) capable database such
 * as Google App Engine.
 * 
 * <p>
 * Copyright © 2010 Bart Verhoeven
 * </p>
 * 
 * <p>
 * This file is part of MovManager.
 * </p>
 * 
 * <p>
 * MovManager is free software: you can redistribute it and/or modify it under
 * the terms of the GNU General Public License as published by the Free Software
 * Foundation, either version 3 of the License, or (at your option) any later
 * version.
 * </p>
 * 
 * <p>
 * MovManager is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
 * </p>
 * 
 * <p>
 * You should have received a copy of the GNU General Public License along with
 * MovManager. If not, see http://www.gnu.org/licenses/.
 * </p>
 * 
 * @author Bart Verhoeven
 * @version 0.1
 */
@PersistenceCapable(identityType = IdentityType.APPLICATION, detachable = "true")
public class Movie implements Serializable {
	private static final long serialVersionUID = 1L;
	/**Unique key identifier for this movie used for data storage.*/
	@PrimaryKey
	@Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
	private Key key;
	/**Movie title.*/
	@Persistent
	private String title;
	/**Movie duration in minutes.*/
	@Persistent
	private int duration;
	/**Movie rating between 0 and 10 with a maximum precision of 1 digit.*/
	@Persistent
	private double rating;
	/**Movie description.*/
	@Persistent
	private String description;
	/**Date on which this movie was released.*/
	@Persistent
	private Date releaseDate;
	/** List of movie directors.*/
	@Persistent
	private List<String> directors;
	/**List of movie writers.*/
	@Persistent
	private List<String> writers;
	/**List of movie actors.*/
	@Persistent
	private List<String> actors;
	/**Studio that distributed this movie.*/
	@Persistent
	private String studio;
	/**List of genres.*/
	@Persistent(mappedBy = "movie", defaultFetchGroup = "true")
	@Element(dependent = "true")
	@Order(extensions = @Extension(vendorName="datanucleus", key="list-ordering", value="name asc"))
	private List<Genre> genres;
	/**Whether the movie has been watched by the user.*/
	@Persistent
	private boolean hasWatched;
	/**Url to movie poster.*/
	@Persistent
	private String moviePoster;
	/**The movie collection to which this movie belongs. A bidirectional mapping is used as required by JDO.*/
	@Persistent
	private MovieCollection movieCollection;

	/**
	 * Default empty constructor required by the Google RPC protocol.
	 */
	public Movie() {
	}

	/**
	 * Creates a new Movie. The following fields are required and need a non-null/non-zero input:
	 * <ul>
	 * 		<li>title</li>
	 * </ul>
	 * 
	 * The other fields are optional and do not require a non-null/non-zero input. 
	 * 
	 * @param	title
	 * 			the title of this movie (required)
	 * @param	duration
	 * 			the duration of this movie in minutes
	 * @param 	rating
	 * 			the rating of this movie
	 * @param 	description
	 * 			the description of this movie
	 * @param 	date
	 * 			the date of this movie
	 * @param	directors
	 * 			the directors of this movie
	 * @param	writers
	 * 			the writers of this movie
	 * @param	genres
	 * 			the genres of this movie
	 * @param	studio
	 * 			the studio that distributed this movie
	 * @param	hasWatched
	 * 			whether the user has watched this movie
	 * @throws	InvalidMovieTitleException 
	 * 			title is empty
	 * @throws	InvalidMovieDurationException
	 * 			duration is negative 
	 * @throws	InvalidMovieRatingException
	 * 			rating is not between 0 and 10 or rating has a precision of more than 1 digit 
	 * @throws	InvalidMovieReleaseDateException 
	 * 			date is in the future
	 */
	public Movie(String title, int duration, double rating, String description, Date date, List<String> directors, List<String> writers, List<String> actors, List<Genre> genres, String studio, boolean hasWatched, String moviePoster) throws InvalidMovieTitleException, InvalidMovieDurationException, InvalidMovieRatingException, InvalidMovieReleaseDateException {
		setTitle(title);
		setDuration(duration);
		setRating(rating);
		setDescription(description);
		setReleaseDate(date);
		setDirectors(directors);
		setWriters(writers);
		setActors(actors);
		setGenres(genres);
		setStudio(studio);
		setHasWatched(hasWatched);
		setMoviePoster(moviePoster);
	}

	/**
	 * Retrieves the unique identifier for this movie.
	 * 
	 * @return	key
	 */
	public Key getKey() {
		return key;
	}

	/**
	 * Sets the unique identifier for this movie. Note that the key is
	 * automatically generated and assigned upon storage in the database. This
	 * method is only here to reconstruct this object upon retrieval from the
	 * database.
	 * 
	 * @param	key
	 *          the unique identifier belonging to this movie
	 */
	public void setKey(Key key) {
		this.key = key;
	}

	/**
	 * Retrieves the title of this movie.
	 * 
	 * @return	title
	 */
	public String getTitle() {
		return title;
	}

	/***
	 * Retrieves the movie collection this movie belongs to.
	 * 
	 * @return	movieCollection
	 */
	public MovieCollection getMovieCollection() {
		return movieCollection;
	}

	/**
	 * Sets the title for this movie
	 * 
	 * @param	title
	 *          the movie title
	 * @throws	InvalidMovieTitleException
	 * 			title is empty
	 */
	public void setTitle(String title) throws InvalidMovieTitleException {
		if (!Movie.isValidTitle(title))
			throw new InvalidMovieTitleException("Empty titles are not allowed");
		
		this.title = title;
	}

	/**
	 * Verifies whether the given title is valid. A title is valid when it has
	 * at least 1 character.
	 * 
	 * @param	title
	 *          the title you want to verify
	 * @return	true if it contains at least 1 character, false otherwise
	 */
	public static boolean isValidTitle(String title) {
		if (title == null || title.equals(""))
			return false;

		return true;
	}

	/**
	 * Retrieves the duration of this movie in minutes.
	 * 
	 * @return	duration
	 */
	public int getDuration() {
		return duration;
	}

	/**
	 * Sets the duration of this movie in minutes.
	 * 
	 * @param	duration
	 *          the duration of this movie in minutes
	 * @throws	InvalidMovieDurationException 
	 * 			duration is negative
	 */
	public void setDuration(int duration) throws InvalidMovieDurationException {
		if (!Movie.isValidDuration(duration))
			throw new InvalidMovieDurationException("Only strictly positive durations are allowed");
		
		this.duration = duration;
	}

	/**
	 * Verifies whether the given duration is valid. Returns true when the
	 * duration is not smaller than 0, false otherwise.
	 * 
	 * @param	duration
	 *        	the duration you want to verify
	 * @return	true if the duration is not smaller than 0
	 */
	public static boolean isValidDuration(int duration) {
		if (duration >= 0)
			return true;

		return false;
	}

	/**
	 * Retrieves the rating of this movie.
	 * 
	 * @return	rating
	 */
	public double getRating() {
		return rating;
	}

	/**
	 * Sets the rating of this movie.
	 * 
	 * @param	rating
	 *      	the rating of this movie
	 * @throws	InvalidMovieRatingException
	 * 			rating is not between 0 and 10 or the rating has a precision of more than 1 digit
	 */
	public void setRating(double rating) throws InvalidMovieRatingException {
		if (!Movie.isValidRatingRange(rating))
			throw new InvalidMovieRatingException("Only ratings between 0 and 10 are allowed");
		
		if (!Movie.isValidRatingPrecision(rating))
			throw new InvalidMovieRatingException("Only ratings with a precision of 1 digit are allowed");
		
		this.rating = rating;
	}

	/**
	 * Verifies whether the given rating is between 0 and 10.
	 * 
	 * @param	rating
	 *          the rating to verify
	 * @return	true if the given rating is between 0 and 10, false otherwise
	 */
	public static boolean isValidRatingRange(double rating) {
		if (rating < 0 || rating > 10)
			return false;

		return true;
	}

	/**
	 * Verifies whether the given rating has a maximum precision of 1 digit.
	 * 
	 * @param	rating
	 * @return	true if it has a maximum precision of 1 digit, false otherwise
	 */
	public static boolean isValidRatingPrecision(double rating) {
		int length = Double.toString(rating).length();

		if (length > 3 && rating != 10)
			return false;

		return true;
	}

	/**
	 * Retrieves the description of this movie.
	 * 
	 * @return	description
	 */
	public String getDescription() {
		return description;
	}

	/**
	 * Sets the description of this movie.
	 * 
	 * @param	description
	 *         	the description of this movie
	 */
	public void setDescription(String description) {
		this.description = description;
	}

	/**
	 * Retrieves the release date of this movie.
	 * 
	 * @return	releaseDate
	 */
	public Date getReleaseDate() {
		return releaseDate;
	}

	/**
	 * Sets the release date of this movie.
	 * 
	 * @param	date
	 *         	the release date of this movie
	 * @throws	InvalidMovieReleaseDateException
	 * 			date is in the future
	 */
	public void setReleaseDate(Date date) throws InvalidMovieReleaseDateException {
		if (!Movie.isValidDate(date))
			throw new InvalidMovieReleaseDateException("Dates in the future are not allowed");
		
		this.releaseDate = date;
	}

	/**
	 * Verifies whether the given date is valid. A release date is valid when it
	 * is not in the future.
	 * 
	 * @param	date
	 * 			the date you want to verify
	 * @return	true if the given date is not in the future, false otherwise
	 */
	public static boolean isValidDate(Date date) {
		if (date != null && new Date().getTime() < date.getTime())
			return false;

		return true;
	}

	/**
	 * Retrieves the list of directors of this movie.
	 * 
	 * @return	directors
	 */
	public List<String> getDirectors() {
		return directors;
	}

	/**
	 * Sets the list of directors of this movie.
	 * 
	 * @param	directors
	 * 			the list of directors
	 */
	public void setDirectors(List<String> directors) {
		this.directors = directors;
	}

	/**
	 * Retrieves the list of writers of this movie.
	 * 
	 * @return	writers
	 */
	public List<String> getWriters() {
		return writers;
	}

	/**
	 * Sets the list of writers of this movie.
	 * 
	 * @param 	writers
	 * 			the list of writers
	 */
	public void setWriters(List<String> writers) {
		this.writers = writers;
	}

	/**
	 * Retrieves the list of actors of this movie.
	 * 
	 * @return	actors
	 */
	public List<String> getActors() {
		return actors;
	}

	/**
	 * Sets the list of actors of this movie.
	 * 
	 * @param	actors
	 * 			the list of actors
	 */
	public void setActors(List<String> actors) {
		this.actors = actors;
	}
	
	/**
	 * Retrieves the studio that distributed this movie.
	 * 
	 * @return	studio
	 */
	public String getStudio() {
		return studio;
	}
	
	/**
	 * Sets the studio that distributed this movie.
	 * 
	 * @param	studio
	 * 			the studio that distributed this movie
	 */
	public void setStudio(String studio) {
		this.studio = studio;
	}
	
	/**
	 * Retrieves the list of genres this movie belongs to.
	 * 
	 * @return	genres
	 */
	public List<Genre> getGenres() {
		return genres;
	}
	
	/**
	 * Sets the list of genres this movie belongs to.
	 * 
	 * @param	genres
	 * 			the list of genres this movie belongs to
	 */
	public void setGenres(List<Genre> genres) {
		this.genres = genres;
	}
	
	/**
	 * Returns whether this movie has been watched already.
	 * 
	 * @return	true if the movie has been watched, false otherwise
	 */
	public boolean hasWatched() {
		return hasWatched;
	}
	
	/**
	 * Sets whether the user has watched the movie or not.
	 * 
	 * @param	hasWatched
	 * 			whether the user has watched the movie
	 */
	public void setHasWatched(boolean hasWatched) {
		this.hasWatched = hasWatched;
	}
	
	/**
	 * Retrieves the url of the poster of this movie.
	 * 
	 * @return	moviePoster
	 */
	public String getMoviePoster() {
		return moviePoster;
	}
	
	/**
	 * Sets the url of the poster of this movie.
	 * 
	 * @param	moviePoster
	 * 			the url of the movie poster
	 */
	public void setMoviePoster(String moviePoster) {
		this.moviePoster = moviePoster;
	}
	
	/**
	 * 2 movies are equal when they have the same title and release date.
	 */
	@Override
	public boolean equals(Object other) {
		if (this == other)
			return true;
		
		if (!(other instanceof Movie))
			return false;
		
		Movie o = (Movie) other;
		if (this.releaseDate == null || o.getReleaseDate() == null)
			return getTitle().equals(o.getTitle());
		
		return getTitle().equals(o.getTitle()) && getReleaseDate().equals(o.getReleaseDate());
	}
}
